home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
C# & Game Programming - A…er's Guide (2nd Edition)
/
Buono 2nd Ed.iso
/
Chapter3
/
AsteroidMiner
/
AsteroidMiner Forms
/
AsteroidMinerGDI+.cs
< prev
next >
Wrap
Text File
|
2004-09-02
|
25KB
|
634 lines
/* AsteroidMiner v2.0 - by Salvatore A. Buono */
using System;
using System.Drawing;
using System.Windows.Forms;
using System.IO; // for file read/write
using GameClasses; // Our common utility class
namespace Games {
public class AsteroidMiner : System.Windows.Forms.Form {
// Constants
const int SCALE = 25;
const string MEDIA_ROOT = @".\media\";
const int TIMER_BASE = 40; // in millis.
const int MAX_STARS = 20; // # of stars
const int MAX_ASTEROIDS = 4; // # of asteroids (if on)
const int MAX_MISSILES = 2; // # of missiles/ship
const int MISSILE_LIFE = 50; // 40 ticks
const int THRUSTER_LIFE = 10; // 10 ticks
const int ASTEROID_SPEED = 10;// how much they move
const int GRAVITY_SPEED = 75; // how often gravity affects you
const int GRAVITY_EFFECT = 5; // how much it effects you
const int DEFAULT_SPEED = 5; // default game speed
const int NUM_LIVES = 5; // determines when the game is over
const int SCORE_ASTEROID = 10; // 10 points for hitting an asteroid
// Image buffer to prevent screen-flicker
Bitmap offScreenBuffer;
// Member fields -- UI elements
private System.Windows.Forms.MainMenu mainMenu1;
private System.Windows.Forms.MenuItem menuItem1;
// Member fields
GameState gameState; // Holds state information
PlayerImageArray player;
AnimatedImage[] exhaustImages;
AnimatedImage[] playerMissiles;
AnimatedImage[] asteroids;
Point[] stars; // background star field
Image imgStar;
GameTimer timer; // Event timer to control all animation
TimedEvent p1Event; // The player event, for easy reference
int HighScore = 0;
private System.Windows.Forms.MenuItem menuItem2; // Game's top score loads from file
int p1LastDirection = 0;
// Main entry-point function
[STAThread] static void Main() {
Application.Run(new AsteroidMiner());
}
#region Set-up & Initialization
// Default constructor
public AsteroidMiner() {
InitializeComponent();
gameState = new GameState();
// Set up timer & timed events
timer = new GameTimer(TIMER_BASE);
TimedEvent newEvent;
// Initialize the two player objects
player = new PlayerImageArray(9);
player.constraintBox = this.ClientRectangle;
// Load images into PlayerImageArrays
player.LoadImage(MEDIA_ROOT + "FireBall.JPG", 0);
player.LoadImages(MEDIA_ROOT + "Ship1-%%.JPG", 1, 8);
player.RescaleImage(SCALE, SCALE);
// player thruster event
newEvent = new TimedEvent(THRUSTER_LIFE,
new TickHandler(PlayerThrust));
newEvent.payload = player;
timer.addEvent(newEvent, 1, "player");
p1Event = newEvent;
// Initialize exhaust images
exhaustImages = new AnimatedImage[9];
for (int i = 0; i < 9; i ++) {
exhaustImages[i] = new AnimatedImage(0, 0);
exhaustImages[i].imageData = Utils.LoadImage(MEDIA_ROOT +
"Exhaust" + i + ".JPG");
exhaustImages[i].RescaleImage(SCALE / 2, SCALE / 2);
}
// Set correct image offsets
exhaustImages[1].imageOffsetX = 14;
exhaustImages[1].imageOffsetY = -5;
exhaustImages[2].imageOffsetX = 9;
exhaustImages[2].imageOffsetY = -9;
exhaustImages[3].imageOffsetX = 0;
exhaustImages[3].imageOffsetY = -6;
exhaustImages[4].imageOffsetX = -8;
exhaustImages[4].imageOffsetY = 6;
exhaustImages[5].imageOffsetX = 1;
exhaustImages[5].imageOffsetY = 17;
exhaustImages[6].imageOffsetX = 8;
exhaustImages[6].imageOffsetY = 20;
exhaustImages[7].imageOffsetX = 11;
exhaustImages[7].imageOffsetY = 17;
exhaustImages[8].imageOffsetX = 20;
exhaustImages[8].imageOffsetY = 6;
// Initialize the background stars
Random rnd = new Random();
stars = new Point[MAX_STARS];
for (int i = 0; i < MAX_STARS; i++)
stars[i] = new Point(rnd.Next(ClientSize.Width), rnd.Next(ClientSize.Height));
imgStar = Utils.LoadImage(MEDIA_ROOT + "FireBall.JPG");
// Initialize player's missiles
playerMissiles = new AnimatedImage[MAX_MISSILES];
for (int i = 0; i < MAX_MISSILES; i++) {
playerMissiles[i] = new AnimatedImage(0, 0);
playerMissiles[i].owner = player;
playerMissiles[i].constraintBox = this.ClientRectangle;
playerMissiles[i].imageData = Utils.LoadImage(MEDIA_ROOT +
"Weapon1.JPG");
playerMissiles[i].RescaleImage(SCALE / 5, SCALE / 5);
// Set up timer & events
newEvent = new TimedEvent(MISSILE_LIFE,
new TickHandler(MoveMissile));
newEvent.payload = playerMissiles[i];
timer.addEvent(newEvent, 1, "playerMissile" + i);
}
// Initialize the asteroids
asteroids = new AnimatedImage[MAX_ASTEROIDS];
for (int i = 0; i < MAX_ASTEROIDS; i++) {
// Note, there are only 4 image positions so strange
// things happen when MAX_ASTEROIDS > 4
asteroids[i] = new AnimatedImage((i % 2 == 1) ? 3 * SCALE :
20 * SCALE, (i < 2 ? 3 * SCALE : 12 * SCALE));
asteroids[i].direction = rnd.Next(1, 8);
asteroids[i].constraintBox = this.ClientRectangle;
asteroids[i].imageData = Utils.LoadImage(MEDIA_ROOT +
"Asteroid" + i + ".JPG");
asteroids[i].RescaleImage(2 * SCALE, 2 * SCALE);
newEvent = new TimedEvent(new TickHandler(MoveAsteroids));
newEvent.payload = asteroids[i];
timer.addEvent(newEvent, ASTEROID_SPEED, "asteroid" + i);
newEvent.isActive = true;
}
newEvent = new TimedEvent(new TickHandler(Gravity));
newEvent.isActive = true;
timer.addEvent(newEvent, GRAVITY_SPEED, "");
// Call Invalidate() every tick
newEvent = new TimedEvent(new TickHandler(ScreenRefresh));
timer.addEvent(newEvent, 1, "invalidate");
newEvent.isActive = true;
}
// (re) Sets positional data, scores, and other game data
public void Setup() {
gameState.currentState = GameState.State.Started;
gameState.currentSpeed = DEFAULT_SPEED;
player.imagePosX = 12 * SCALE - (player.imageWidth >> 1);
player.imagePosY = 8 * SCALE;
if (player.score > HighScore)
SaveHighScore(player.score);
HighScore = LoadHighScore();
if(HighScore < player.score) {
HighScore = player.score;}
player.score = 0;
player.deaths = 0;
player.isActive = true;
player.direction = AnimatedImage.NORTH;
for (int i = 0; i < MAX_MISSILES; i++) {
playerMissiles[i].isActive = false;
}
for (int i = 0; i < MAX_ASTEROIDS; i++) {
asteroids[i].RescaleImage(2 * SCALE, 2 * SCALE);
asteroids[i].isActive = true;
}
timer.Start();
}
// Form's initialize method - we initialize all form related items
private void InitializeComponent() {
this.mainMenu1 = new System.Windows.Forms.MainMenu();
this.menuItem1 = new System.Windows.Forms.MenuItem();
this.menuItem2 = new System.Windows.Forms.MenuItem();
//
// mainMenu1
//
this.mainMenu1.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] {
this.menuItem1,
this.menuItem2});
//
// menuItem1
//
this.menuItem1.Index = 0;
this.menuItem1.Text = "Asteroid Miner";
this.menuItem1.Click += new System.EventHandler(this.SinglePlayer_Click);
//
// menuItem2
//
this.menuItem2.Index = 1;
this.menuItem2.Text = "";
//
// AsteroidMiner
//
this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
this.BackColor = System.Drawing.Color.Black;
this.ClientSize = new System.Drawing.Size(600, 400);
this.Menu = this.mainMenu1;
this.Name = "AsteroidMiner";
this.Text = "Asteroid Miner";
this.Load += new System.EventHandler(this.AsteroidMiner_Load);
}
#endregion
#region Game-play functions
// Start up the player's thrusters
public void EngageThrusters(Player player) {
if (player.isActive) {
if (player == player)
p1Event.Reset(); // reset the timer from now
Utils.PlaySound(MEDIA_ROOT + "Thrust.wav");
}
return;
}
// Fire a missile (if any available) from the given player
public void FireMissile(Player player, AnimatedImage[] missiles) {
if (!player.isActive)
return;
for (int i = 0; i < MAX_MISSILES; i++) {
// shoot the first missile that's not already out there!
if (!missiles[i].isActive) {
// set missiles properties so that it will get
// properly animated
missiles[i].isActive = true;
missiles[i].direction = player.direction;
missiles[i].imagePosX = player.imagePosX;
missiles[i].imagePosY = player.imagePosY;
TimedEvent missileEvent = timer.getEvent
("playerMissile" + i);
missileEvent.Reset();
Utils.PlaySound(MEDIA_ROOT + "Fire.wav");
break;
}
}
}
public void ExertGravitationalPull(Player player) {
if (player.imagePosX != ClientSize.Width >> 1)
player.imagePosX += (player.imagePosX >
(ClientSize.Width >> 1)) ? -GRAVITY_EFFECT : GRAVITY_EFFECT;
if (player.imagePosY != ClientSize.Height >> 1)
player.imagePosY += (player.imagePosY >
(ClientSize.Height >> 1)) ? -GRAVITY_EFFECT : GRAVITY_EFFECT;
}
#endregion
#region Collision Detection Routines
// Ships can run into the asteroids and missiles.
// We'll assume that the missiles will detect their collisions
// so we only have to worry about the others.
public bool CheckShipCollision(Player player) {
Player opponent = player;
// Asteroid collision?
for (int i = 0; i < MAX_ASTEROIDS; i++) {
if (asteroids[i].isActive) {
if (asteroids[i].Intersects(player)) {
ShipCollided(player);
return true;
}
}
}
return false;
}
// Asteroids can run into ships & missiles. We'll assume that
// missiles will detect their collisions themselves since they
// update frequently.
public bool CheckAsteroidCollision() {
for (int i = 0; i < MAX_ASTEROIDS; i++) {
if (asteroids[i].isActive) {
if (asteroids[i].Intersects(player)) {
ShipCollided(player);
return true;
}
}
}
return false;
}
// Missiles can only run into asteroids.
public bool CheckMissileCollision(AnimatedImage missile) {
if (missile.isActive) {
for (int i = 0; i < MAX_ASTEROIDS; i++) {
if (missile.Intersects(asteroids[i])) {
AsteroidCollided(asteroids[i]);
missile.isActive = false;
Utils.PlaySound(MEDIA_ROOT + "Hit.wav");
return true;
}
}
}
return false; // Must not have hit anything!
}
// The ship ran into something! Add to the death count and
// end the game if we're out of lives.
public void ShipCollided(Player victim) {
if (!victim.isActive)
return; // He's already dead!
victim.isActive = false;
victim.direction = 0; // index of our explosion image
Utils.PlaySound(MEDIA_ROOT + "Hit.wav");
victim.deaths++;
if(victim.deaths >= NUM_LIVES) {
gameState.currentState = GameState.State.Stopped;
if (victim.score > HighScore) {
SaveHighScore(victim.score);
}
}
}
// The asteroid was hit! Cycle through its different scaled
// display & increment the player's score.
public void AsteroidCollided(AnimatedImage victim) {
int scaleFactor = (victim.imageWidth <= (SCALE / 2)) ?
SCALE * 2 : victim.imageWidth / 2;
victim.RescaleImage(scaleFactor, scaleFactor);
player.score += SCORE_ASTEROID;
}
#endregion
#region OnPaint
protected override void OnPaint(PaintEventArgs e) {
Graphics gOffScreen; // Graphics context for offscreen buffer
Graphics g = e.Graphics; // Screen's graphics object
// If the size of the window has changed, we need a new buffer
if (offScreenBuffer == null ||
offScreenBuffer.Width != this.ClientSize.Width ||
offScreenBuffer.Height != this.ClientSize.Height) {
if (ClientSize.Width == 0 || ClientSize.Height == 0)
return;
offScreenBuffer = new Bitmap(this.ClientSize.Width,
this.ClientSize.Height);
}
// We paint just as if it were the screen. The function need not
// know it is being buffered.
gOffScreen = Graphics.FromImage(offScreenBuffer);
DoPaint(gOffScreen); // Call normal paint method
gOffScreen.Dispose(); // Free up resources right away
// We draw our buffered image onto the screen in one operation.
// This could be improved further still by using clip regions.
g.DrawImage(offScreenBuffer, 0, 0);
}
public void DoPaint(Graphics g) {
// Redraw the background
g.FillRectangle(Brushes.Black, 0, 0,
this.Size.Width, this.Size.Height);
// Write start-up text if game is stopped
if (gameState.currentState == GameState.State.Stopped) {
Brush YellowBrush = Brushes.Yellow;
Font LargeAlgerianFont = new Font("Algerian", 24);
Font MidAlgerianFont = new Font("Algerian", 18);
g.DrawString("C# and Game Programming", LargeAlgerianFont, YellowBrush, 2 * SCALE, SCALE);
g.DrawString("A Beginner's Guide", LargeAlgerianFont, YellowBrush, 9 * SCALE, 5 * SCALE);
g.DrawString("By Salvatore A. Buono", MidAlgerianFont, YellowBrush, 2 * SCALE, 14 * SCALE);
} else {
// Draw the scores
Font normal = new Font("Time New Roman", 14, FontStyle.Bold);
if(player.score < HighScore) {
g.DrawString(HighScore.ToString(), normal, Brushes.Blue,
4 * SCALE, SCALE);}
else {
g.DrawString(player.score.ToString(), normal, Brushes.Blue,
4 * SCALE, SCALE);}
g.DrawString(player.score.ToString(), normal, Brushes.Blue,
16 * SCALE, SCALE);
// Draw the stars
for(int i = 0; i < MAX_STARS; i++)
g.DrawImage(imgStar, stars[i].X, stars[i].Y, 10, 10);
// Draw player 1 & its appropriate exhaust
player.Display(g);
if (p1Event.isActive) {
exhaustImages[player.direction].imagePosX = player.imagePosX;
exhaustImages[player.direction].imagePosY = player.imagePosY;
exhaustImages[player.direction].Display(g);
}
// Draw missiles
for (int i = 0; i < MAX_MISSILES; i ++) {
if (playerMissiles[i].isActive)
playerMissiles[i].Display(g);
}
// Draw asteroids
for (int i = 0; i < MAX_ASTEROIDS; i++) {
asteroids[i].Display(g);
}
}
}
#endregion
#region OnKey
protected override void OnKeyDown(KeyEventArgs e) {
if (gameState.currentState != GameState.State.Started)
return;
// Player controls
switch(e.KeyCode) {
case Keys.Up:
p1LastDirection = player.direction;
EngageThrusters(player);
break;
case Keys.Left:
if (player.isActive)
player.RotateDirection(1);
break;
case Keys.Right:
if (player.isActive)
player.RotateDirection(-1);
break;
case Keys.Down:
if (!player.isActive)
player.isActive = true;
player.RandomizePosition();
break;
case Keys.NumPad0:
case Keys.Space:
FireMissile(player, playerMissiles);
break;
// Exit game
case Keys.Escape:
ExitApplication();
break;
}
base.OnKeyDown(e);
}
#endregion
#region TimedEvent Tick Handlers
// Implements the player's thruster push. Called repetitively until
// the timer counts down to 0.
public void PlayerThrust(TimedEvent e, Object obj) {
if (gameState.currentState == GameState.State.Stopped) {
e.isActive = false;
return;
}
Player player = (Player) obj;
int tempDir = player.direction;
player.direction = p1LastDirection;
player.Animate();
player.Animate();
player.direction = tempDir;
player.WrapInBox();
CheckShipCollision(player);
}
// Implements the missile's movement. Called automatically until
// the timer counts down to 0.
public void MoveMissile(TimedEvent e, Object obj) {
if (gameState.currentState == GameState.State.Stopped) {
e.isActive = false;
return;
}
// Get a proper missile object from the event
AnimatedImage missile = (AnimatedImage) obj;
for(int i = 0; i < gameState.currentSpeed; i++) {
missile.Animate();
missile.WrapInBox();
// See if we hit anything!
CheckMissileCollision(missile);
}
// disable the missile so it can be fired again
if (e.tickCounter <= 1)
missile.isActive = false;
}
// Implements the asteroid's random movement. This is called
// continously throughout the game.
public void MoveAsteroids(TimedEvent e, Object obj) {
if (gameState.currentState == GameState.State.Stopped) {
e.isActive = false;
return;
}
Random rnd = new Random();
for(int i = 0; i < MAX_ASTEROIDS; i++) {
if (rnd.Next(10) > 7) // Change the direction ~70% of the time
asteroids[i].direction = rnd.Next(1, 8);
asteroids[i].Animate();
asteroids[i].WrapInBox();
CheckAsteroidCollision();
}
}
// Implements the gravity (from an unseen field -- that only
// affects ships -- located at the center of the screen).
public void Gravity(TimedEvent e, Object obj) {
if (gameState.currentState == GameState.State.Stopped) {
e.isActive = false;
return;
}
if (player.isActive){ // Doesn't pull "dead" ships
ExertGravitationalPull(player);
CheckShipCollision(player);
}
Utils.PlaySound(MEDIA_ROOT + "Gravity.wav");
}
// Regular refresh function. The screen will redraw every tick.
public void ScreenRefresh(TimedEvent e, Object obj) {Invalidate();}
#endregion
#region Menu Click Handler
private void SinglePlayer_Click(object sender, System.EventArgs e) {
gameState.currentMode = GameState.Mode.SinglePlayer;
Setup();
}
#endregion
#region File Read/Write
// Utility function to read the high score from a file
public int LoadHighScore() {
StreamReader file;
string strTemp = "";
int score = 0;
try {
file = new StreamReader(@".\HighScore.txt");
for (int i = 0; i < 4; i++)
strTemp = file.ReadLine();
score = int.Parse(strTemp.Substring(0, strTemp.IndexOf("\t")));
file.Close();
} catch (Exception e) {Console.WriteLine(e);}
return score;
}
// Utility function to write the high score from a file
public void SaveHighScore(int score) {
StreamWriter file;
try {
file = new StreamWriter(@".\HighScore.txt");
file.WriteLine("Asteroid Miner High Scores");
file.WriteLine("--------------------------");
file.WriteLine();
file.WriteLine(score + "\t" + DateTime.Now);
file.Flush();
file.Close();
} catch (Exception e) {Console.WriteLine(e);}
}
#endregion
// Clean exit function
public void ExitApplication() {
if (MessageBox.Show ("Are you sure you want to quit?", "End Game",
MessageBoxButtons.YesNo) == DialogResult.Yes)
Application.Exit();
}
// The default Invalidate clears the entire screen creating a very
// undesireable flicker. This override allows the paint method to
// control it's redraw.
public new void Invalidate() {
OnPaint(new PaintEventArgs(Graphics.FromHwnd(this.Handle),
new Rectangle(new Point(0, 0), this.Size)));
}
// The system occasionally also calls OnPaintBackground which will
// mean the screen will be cleared, painted with the background
// color, then repainted. This is another cause of flicker, so we
// just override it and tell it not to do anything!
protected override void OnPaintBackground(PaintEventArgs e) {
// Do nothing!
}
private void AsteroidMiner_Load(object sender, System.EventArgs e) {
}
}
}